<template>
  <div class="container">
    <h1>🖌️ Yjs 协同白板</h1>

    <!-- 工具栏 -->
    <div class="toolbar">
      <label>
        🎨 颜色：
        <input type="color" v-model="color" />
      </label>
      <label>
        📏 粗细：
        <input type="range" min="1" max="20" v-model="size" />
      </label>
      <!-- 当前的状态：是画笔还是橡皮擦 -->
      <button @click="toggle" :class="{ active: isEraser }">
        {{ isEraser ? "橡皮擦" : "画笔🖌️" }}
      </button>
      <button @click="clearBorad">🧹 清空画布</button>
    </div>

    <canvas ref="canvasRef" class="board" width="800" height="500"></canvas>
  </div>
</template>

<script setup lang="ts">
import * as Y from "yjs";
import { ref, onMounted } from "vue";

// 定义绘制的点相关的信息
// 绘制的线段是由一个一个点组成的，要形成一个线段，至少要有两个点
// x1 和 y1 记录的是鼠标按下的时候的起点
// x2 和 y2 记录的是鼠标移动到的新的地方，相当于终点
type Stroke =
  | {
      x1: number;
      y1: number;
      x2: number;
      y2: number;
      color: string;
      size: number;
      isEraser: boolean;
    }
  | { type: "clear" }; // 清空

// 声明一些响应式数据
const canvasRef = ref<HTMLCanvasElement | null>(null); // canvas 画板的引用
const color = ref("#000"); // 画笔的颜色
const size = ref(4); // 画笔的粗细
const isEraser = ref(false); // 是否是橡皮擦，默认是画笔，所以初始值为 false

// 声明一些和 yjs 相关的数据
const doc = new Y.Doc(); // 创建一个 Yjs 副本
const strokes = doc.getArray<Stroke>("strokes"); // 创建一个数组类型的共享数据，数组里面存储的是点的信息
const channel = new BroadcastChannel("yjs-whiteboard");

// 这里就是一些辅助变量，不需要声明为响应式
let ctx: CanvasRenderingContext2D;
let drawing = false; // 当前是否处于正在绘制的状态
let lastX = 0; // 记录最终的 x 坐标
let lastY = 0; // 记录最终的 y 坐标

// 书写一个类型收缩的方法
function isClearStroke(s: Stroke): s is { type: "clear" } {
  return "type" in s && s.type === "clear";
}

// 绘制方法
function drawLine(info: Stroke) {
  if (isClearStroke(info)) return;
  // 先进行画布配置
  ctx.strokeStyle = info.isEraser ? "#fff" : info.color;
  ctx.lineWidth = info.size;
  ctx.lineCap = "round";
  ctx.beginPath();
  ctx.moveTo(info.x1, info.y1); // 落笔，鼠标移动到哪一个点
  ctx.lineTo(info.x2, info.y2); // 先把线描出来
  ctx.stroke(); // 绘制线
}

// 按下鼠标的时候相关的操作
function handleMouseDown(e: MouseEvent) {
  drawing = true; // 表示当前进入了绘制状态

  // 获取 canvas 相对于浏览器窗口的一个偏移量
  const rect = canvasRef.value!.getBoundingClientRect();

  // 计算 x 和 y 坐标
  lastX = e.clientX - rect.left;
  lastY = e.clientY - rect.top;
  // 记录鼠标按下时的一个坐标点
}

// 移动鼠标的时候相关的操作
function handleMouseMove(e: MouseEvent) {
  // 核心逻辑：不断的更新 x 和 y 坐标

  if (!drawing) return; // 如果当前不处于绘制状态，直接结束

  const rect = canvasRef.value!.getBoundingClientRect(); // 再次获取 canvas 的位置信息

  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;

  // 组装一个 stroke 对象，这个 stroke 就可以理解为一个小小的线段
  const stroke = {
    x1: lastX,
    y1: lastY,
    x2: x,
    y2: y,
    color: color.value,
    size: size.value,
    isEraser: isEraser.value,
  };

  // 存储到 yjs 副本里面，为了后面同步给其他标签页
  strokes.push([stroke]);

  // 接下来进行一个本地的绘制
  drawLine(stroke);

  // 更新 lastX 和 lastY，为下一次绘制做准备，这一次的终点就会变成下一次的起点
  lastX = x;
  lastY = y;
}

// 抬起鼠标的时候相关的操作
function handleMouseUp(e: MouseEvent) {
  drawing = false;
}

// 该方法是在 yjs 同步过来远程的绘制信息的时候调用
function renderStroke(s: Stroke) {
  if (isClearStroke(s)) {
    // 说明当前是清空画布
    ctx.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
    return;
  }
  // 进行绘制操作
  drawLine(s);
}

function clearBorad() {
  strokes.push([
    {
      type: "clear",
    },
  ]);
}

function toggle() {
  isEraser.value = !isEraser.value;
}

onMounted(() => {
  const canvas = canvasRef.value!;
  ctx = canvas.getContext("2d")!;

  canvas.addEventListener("mousedown", handleMouseDown);
  canvas.addEventListener("mousemove", handleMouseMove);
  canvas.addEventListener("mouseup", handleMouseUp);
  canvas.addEventListener("mouseleave", handleMouseUp);

  // 一开始画布就需要根据 yjs 里面的内容进行一个初始化
  strokes.toArray().forEach(renderStroke);

  // 当发生更新的时候，需要将更新广播出去
  doc.on("update", (update) => {
    channel.postMessage(update);
  });

  channel.addEventListener("message", (e) => {
    Y.applyUpdate(doc, e.data);
  });

  strokes.observe((event) => {
    // event.changes.added 这是一个列表，包含这一次变化中新增到数组的元素
    event.changes.added.forEach((item) => {
      // item.content 表示这一次的新增内容
      // 继续调用 getContent 获取到每一笔（其实就是一个 stroke 对象）
      item.content.getContent().forEach(renderStroke);
    });
  });
});
</script>

<style scoped>
.container {
  padding: 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  font-family: "Segoe UI", sans-serif;
  background: linear-gradient(145deg, #f3f4f6, #e5e7eb);
  min-height: 100vh;
}
h1 {
  color: #111827;
}
.toolbar {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1rem;
}
canvas.board {
  border: 2px solid #d1d5db;
  background: #ffffff;
  border-radius: 12px;
  cursor: crosshair;
}
button {
  padding: 0.5rem 1rem;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.2s;
}
button:hover {
  background: #2563eb;
}
button.active {
  background: #f97316;
}
</style>
